# -*- coding: latin-1 -*-

# python
from email.Message import Message
from email.Header import Header
import smtplib
import csv, re
from cStringIO import StringIO
from sets import Set
from types import ListType
import random
try:
    set
except NameError:
    from sets import Set as set
    
try:
    import Products.CMFCore.permissions as CMFCorePermissions
except ImportError:
    from Products.CMFCore import CMFCorePermissions    


# zope
from AccessControl import ClassSecurityInfo
from AccessControl.SecurityManagement import newSecurityManager
from Acquisition import aq_base

# archetypes
from Products.Archetypes.public import BaseContent, registerType
from Products.Archetypes.public import BaseBTreeFolder, BaseBTreeFolderSchema
from Products.CMFCore import permissions
from Products.CMFCore.permissions import setDefaultRoles, ListFolderContents
from Products.CMFCore.permissions import AddPortalMember
from Products.CMFCore.utils import getToolByName

# AT content types, for 2.1 compatibility
#from Products.ATContentTypes import ATBTreeFolder as BaseBTreeFolder

# product
from config import PROJECTNAME, VIEW_CONTENTS_PERMISSION, \
                   EDIT_CONTENTS_PERMISSION, ADD_CONTENTS_PERMISSION
from schemas import mxmContacts_person_schema, mxmContacts_organization_schema, \
                    mxmContacts_resource_schema, mxmContacts_group_schema, \
                    REGIONS, ECONOMICBLOCS

from vCardExporter import vCardExporter

CSV_KEY = 'mxm_csv_file' # for saving csv data in session variable
EMPLOYED_REF = 'mxmContacts_employed_at'




class mxmContacts(BaseBTreeFolder):

    """
    The base folder for contact information.
    """

    content_icon = 'mxmContacts_icon.gif'

    allowed_content_types = (
        'mxmContactsPerson', 'mxmContactsOrganization', 'mxmContactsResource', 
        'mxmContactsGroup',
    )

    global_allow = 1

    schema = BaseBTreeFolderSchema

    actions = (
        {
            'id': 'view',
            'name': 'View',
            'action': 'string:${object_url}/mxmContacts_view',
            'permissions': (VIEW_CONTENTS_PERMISSION,),
            'visible':1,
        },
        {
            'id': 'folderContents',
            'name': 'Contents',
            'action': 'string:${object_url}/folder_contents',
            'permissions': (ListFolderContents,),
            'visible':0,
        },
        {
            'id': 'edit',
            'name': 'Edit',
            'action': 'string:${object_url}/base_edit',
            'permissions': (EDIT_CONTENTS_PERMISSION,),
            'visible':1,
        },
        {
            'id': 'mxmContacts_csv_import',
            'name': 'CSV import',
            'action': 'string:${object_url}/mxmContacts_csv_import',
            'permissions': (EDIT_CONTENTS_PERMISSION,),
            'visible':1,
        },
        {
            'id': 'mxmContacts_csv',
            'name': 'CSV export',
            'action': 'string:${object_url}/mxmContacts_csv',
            'permissions': (EDIT_CONTENTS_PERMISSION,),
            'visible':1,
        },
        {
            'id': 'metadata',
            'name': 'Properties',
            'action': 'string:${object_url}/base_metadata',
            'permissions': (EDIT_CONTENTS_PERMISSION,),
            'visible':0,
        },
        {
            'id': 'references',
            'name': 'References',
            'action': 'string:${object_url}/reference_edit',
            'permissions': (EDIT_CONTENTS_PERMISSION,),
            'visible':0,
        },
    )

    # Standard security settings
    security = ClassSecurityInfo()
    security.declareObjectProtected(VIEW_CONTENTS_PERMISSION)



    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getContactTypes')
    def getContactTypes(self):
        "Returns getContactTypes "
        return ['mxmContactsPerson', 'mxmContactsOrganization', 
                'mxmContactsResource', 'mxmContactsGroup']


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'contactPersonFieldNames')
    def contactPersonFieldNames(self):
        """
        Returns list of contactPerson field names
        """
        fields = mxmContacts_person_schema.filterFields(isMetadata=0)
        return [field.getName() for field in fields]


    def _site_encoding(self):
        "Returns the site encoding"
        portal_properties = self.portal_properties
        site_props = portal_properties.site_properties
        return site_props.default_charset or 'utf-8'


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'exportCSV')
    def exportCSV(self, contacts=None, fields=None, delimiter='comma', 
                  quote_char='double_quote', coding='latin-1',
                  export_type='mxmContactsPerson'):

        """
        Exports a list of contacts as a CSV file.
        contacts: if None it exports all contact persons in the folder.
        fields: field names to export
        """
        if contacts is None:
            contacts = self.listFolderContents(contentFilter={'portal_type':export_type})
        delim_map = {'tabulator':'\t', 'semicolon':';', 'colon':':', 'comma':',', 'space':' ', }
        delimiter = delim_map[delimiter]
        quote_map = {'double_quote':'"', 'single_quote':"'", }
        quote_char = quote_map[quote_char]
        # generate result
        if fields is None:
            result = ''
        else:
            rows = [fields]
            for contact in contacts:
                row = []
                for fieldname in fields:
                    field = contact.schema[fieldname]
                    value = getattr(contact, field.accessor)()
                    if field.type == 'reference':
                        if value:
                            if isinstance(value, ListType):
                                value = ', '.join([v.title_or_id() for v in value])
                            else:
                                value = value.title_or_id()
                        else:
                            value = ''
                    if value.find('\r\n') != -1:
                        value.replace('\r\n', '\n')
                    row.append(value)
                rows.append(row)
            # convert lists to csv string
            ramdisk = StringIO()
            writer = csv.writer(ramdisk, delimiter=delimiter)
            writer.writerows(rows)
            result = ramdisk.getvalue()
            ramdisk.close()
        # encode the result
        charset = self._site_encoding()
        if coding:
            result = result.decode(charset).encode(coding)
        else:
            coding = charset
        # set headers and return
        setheader = self.REQUEST.RESPONSE.setHeader
        setheader('Content-Length', len(result))
        setheader('Content-Type', 'text/x-comma-separated-values; charset=%s' % coding)
        setheader('Content-Disposition', 'filename=%s.csv' % self.getId())
        return result


    security.declareProtected(EDIT_CONTENTS_PERMISSION, 'csv_parser')
    def csv_parser(self, delimiter='comma', quote_char='double_quote', coding='latin-1'):
        """Don't import HectoMegaByte files, or you are asking for it!"""
        delim_map = {'tabulator':'\t', 'semicolon':';', 'colon':':', 'comma':',', 'space':' ', }
        delimiter = delim_map[delimiter]
        quote_map = {'double_quote':'"', 'single_quote':"'", }
        quote_char = quote_map[quote_char]
        charset = self._site_encoding()
        csv_file_content = self.REQUEST.SESSION.get(CSV_KEY, '')
        # this sucks! double memory usage
        f = StringIO(csv_file_content.decode(coding, 'replace').encode(charset))
        reader = csv.reader(f, delimiter=delimiter)
        rows = []
        for row in reader:
            rows.append(row)
        return rows


    def _del_session_cache(self):
        ses = self.REQUEST.SESSION
        if ses.has_key(CSV_KEY):
            del ses[CSV_KEY]


    security.declareProtected(EDIT_CONTENTS_PERMISSION, 'uploadCSVFile')
    def uploadCSVFile(self, REQUEST=None):
        """
        Read uploaded csv file and save it in sessions variable
        """
        session = self.REQUEST.SESSION
        csv_file = self.REQUEST.get('csv_file', None)
        if csv_file is None:
            self._del_session_cache()
        else:
            session[CSV_KEY] = csv_file.read()
        self.REQUEST.RESPONSE.redirect('%s/mxmContacts_csv_format' % self.absolute_url())


    security.declareProtected(EDIT_CONTENTS_PERMISSION, 'contactTypeFieldNames')
    def contactTypeFieldNames(self, import_type):
        "Returns the field names for contact types"
        if import_type == 'mxmContactsPerson':
            schema = mxmContacts_person_schema
        elif import_type == 'mxmContactsOrganization':
            schema = mxmContacts_organization_schema
        fields = schema.filterFields(isMetadata=0)
        return [field.getName() for field in fields if field.type in ('string','text',) and field.getName() != 'id']


    def _getFieldsMap(self, REQUEST):
        "Returns the fields map from the request"
        fields_map = {}
        n_fields = REQUEST.get('n_fields', 0)
        for n in range(n_fields):
            fieldname = REQUEST.get('fields_map_%s' % n, '')
            if fieldname != '*discard*':
                fields_map.setdefault(fieldname, []).append(n)
        return fields_map


    def _unique(self, dicts):
        "Removes duplicates from the import map."
        uniques = {}
        for dict_ in dicts:
            keys = dict_.keys()
            keys.sort()
            temp = []
            for key in keys:
                val = dict_[key]
                temp.append((key, val))
            uniques[tuple(temp)] = dict_
        vals = uniques.values()
        return vals


    def _addContacts(self, import_type, import_maps):
        "Add contacts to self."
        import_maps = self._unique(import_maps)
        now = self.ZopeTime()
        d_time = now.strftime('%Y%m%dZ%H%M%S')
        l_import_type = import_type.lower()
        id_prefix = '%s.%s.%%s' % (l_import_type, d_time)
        for i in xrange(len(import_maps)):
            id = id_prefix % i
            self.invokeFactory(import_type, id)
            obj = self._getOb(id)
            import_map = import_maps[i]
            for key, val in import_map.items():
                setattr(obj, key, val)
            obj.reindexObject()
            # if more objects are imported than can be held in the cache
            # it slows down. So we do partial commits for every 200 objects
            if i and not i % 200:
                get_transaction().commit()
        get_transaction().commit()


    def _field_types(self, import_type):
        "Returns a dict with mapping of field and its type"
        if import_type == 'mxmContactsPerson':
            schema = mxmContacts_person_schema
        elif import_type == 'mxmContactsOrganization':
            schema = mxmContacts_organization_schema
        fields = schema.filterFields(isMetadata=0)
        field_types = {}
        for field in fields:
            field_types[field.getName()] = field.type
        return field_types

    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'importCSV')
    def importCSV(self, delimiter='comma', quote_char='double_quote', 
                  coding='latin-1', import_type=None, link_via_title=0, 
                  REQUEST=None):
        "Convert the uploaded csv data to contact types"
        fields_map = self._getFieldsMap(REQUEST)
        delimiter_map = {'string':' ','text':'\n'} # What types should be joined how
        field_types = self._field_types(import_type)
        rows = self.csv_parser(delimiter, quote_char, coding)
        # get the data from the fields and put them in dict of {attrname:value}
        import_maps = []
        for row in rows:
            import_map = {}
            for key, val in fields_map.items():
                vals = []
                # find the values
                for v in val:
                    if row[v]:
                        vals.append(row[v])
                field_type = field_types[key]
                delimiter = delimiter_map[field_type]
                import_map[key] = delimiter.join(vals)
            import_maps.append(import_map)
        
        # add the contacts
        self._addContacts(import_type, import_maps)
        # delete file from session object as it can be several MB
        self._del_session_cache()
        
        # if link_via_title is true, we must relate organizations to persons via 
        # the organizations title. Often contacts are imported from flat tables, so this
        # can be the most practical way to do it.
        if link_via_title:
            org_titles = {} # map organization titles to organisations
            for org in self.contentValues(filter={'portal_type':'mxmContactsOrganization'}):
                org_titles.setdefault(org.Title(), []).append(org)

            person_titles = {}
            for person in self.contentValues(filter={'portal_type':'mxmContactsPerson'}):
                if person.getOrganizationName() and (not person.getOrganization()):
                    person_titles.setdefault(person.getOrganizationName(), []).append(person)

            for org_title, persons in person_titles.items():
                for person in persons:
                    orgs = org_titles.get(org_title, [])
                    for org in orgs:
                        person.addReference(org.UID(), EMPLOYED_REF)
        if not REQUEST is None:
            REQUEST.RESPONSE.redirect('%s?portal_status_message=Import%%20succeded' % self.absolute_url())


from IdGenerator import IdGenerator
from ZODB.POSException import ConflictError

class mxmContactsPerson(BaseContent):

    content_icon = 'mxmContacts_person_icon.gif'

    global_allow = 0

    _member_id = None # the member id of the contact

    schema = mxmContacts_person_schema

    actions = (
        {
            'id': 'view',
            'name': 'View',
            'action': 'string:${object_url}/mxmContacts_person_view',
            'permissions': (VIEW_CONTENTS_PERMISSION,)
        },
#        {
#            'id': 'join_form',
#            'name': 'Make Member',
#            'action': 'string:${object_url}/join_form',
#            'permissions': (AddPortalMember,),
#            'visible':1,
#        },
        {
            'id': 'metadata',
            'name': 'Properties',
            'action': 'string:${object_url}/base_metadata',
            'permissions': (EDIT_CONTENTS_PERMISSION,),
            'visible':0,
        },
        {
            'id': 'vcard_export',
            'name': 'vcard export',
            'action': 'string:${object_url}/vcard_export',
            'permissions': (VIEW_CONTENTS_PERMISSION,),
            'visible':1,
        },
    )

    # Standard security settings
    security = ClassSecurityInfo()
    security.declareObjectProtected(VIEW_CONTENTS_PERMISSION)


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'vcard_export')
    def vcard_export(self):
        "Exports contact data in VCARD format"
        result = vCardExporter().vCard_Personals([self])
        setheader = self.REQUEST.RESPONSE.setHeader
        setheader('Content-Length', len(result))
        setheader('Content-Type', 'text/directory; charset=\"%s\"' % self._site_encoding())
        setheader('Content-Disposition', 'filename=%s.vcf' % self.getId())
        return result


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getMxmContactsPerson')
    def getMxmContactsPerson(self):
        "Returns self"
        return self

    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'Title')
    def Title(self):
        "Returns the title of the object"
        member = self.getContactMember()
        fullname = ''
        if member:
            fullname = member.getProperty('fullname', '')
        if not fullname:
            fullname = ('%s %s' % (self.getFirstname(), self.getLastname(),)).strip()
        # formatting title
#        if fullname and nickname:
#            return '%s (%s)' % (fullname, nickname)
        if fullname:
            return fullname
        elif self.getTitle():
            return self.getTitle()
        return '???'

    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'title_or_id')
    def title_or_id(self):
        title = self.Title()
        if title:
            return title
        return self.getId()

    #security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getEmail')
    #def getEmail(self):
    #    "Returns the title of the object"
    #    member = self.getContactMember()
    #    if member:
    #        return member.getProperty('email', self.email)
    #    return self.email


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'summary')
    def summary(self):
        "Returns short title/description"
        return ' '.join([' '.join([self.getFirstname(), self.getLastname()]), '(%s)' % self.Title()])

    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'Description')
    Description = summary

    def _site_encoding(self):
        "Returns the site encoding"
        portal_properties = self.portal_properties
        site_props = portal_properties.site_properties
        return site_props.default_charset or 'utf-8'

    security.declarePrivate('getMemberIds')
    def getMemberIds(self):
        "Returns list of all member ids"
        m_tool = getToolByName(self, 'portal_membership')
        return m_tool.listMemberIds()

    security.declarePrivate('getMemberItems')
    def getMemberItems(self):
        "Returns [(id, fullname),] for all members"
        m_tool = getToolByName(self, 'portal_membership')
        return [(m.getMemberId(), m.getProperty('fullname', '')) for m in m_tool.listMembers()]

    security.declareProtected(AddPortalMember, 'proposeMemberId')
    def proposeMemberId(self):
        "Suggests a member id from the value of title_or_id"
        usedIds = self.getMemberIds()
        title = self.title_or_id().strip()
        title = title.decode(self._site_encoding())
        if not title:
            title = u'member'
        idGen = IdGenerator(title, usedIds)
        suggestedId = idGen.generate()
        return suggestedId

    # membership methods

    security.declarePublic('memberCanAdd')
    def memberCanAdd(self):
        "Returns true if the current member can add this contact as a member"
        m_tool = getToolByName(self, 'portal_membership')
        member = m_tool.getAuthenticatedMember()
        return member.has_permission('Add portal member', self) and not self.contactIsMember()


    security.declarePublic('contactIsMember')
    def contactIsMember(self):
        "Returns true if the contact is a member"
        return self.getContactMember() != None
        
    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getContactResources')
    def getContactResources(self):
        "Returns list of Resources under this organization"
        return self.getBRefs(relationship='resource_owner')        


    security.declarePrivate('getContactMember')
    def getContactMember(self):
        "Returns the contact persons member object"
        m_tool = getToolByName(self, 'portal_membership')
        memberId = self.getMemberId()
        if not memberId is None:
            # a bare exception if another membership_tool doesn't 
            # return a None, for non-existing members
            try:
                member = m_tool.getMemberById(memberId)
                return member
            except:
                return None


    security.declareProtected(EDIT_CONTENTS_PERMISSION, 'setMemberId') # AddPortalMember
    def setMemberId(self, username):
        "Saves the id of the member"
        self._member_id = username


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getMemberId')
    def getMemberId(self):
        "Returns the id of the member"
        return self._member_id


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getMemberUrl')
    def getMemberUrl(self):
        "Returns the url of the member"
        memberid = self.getMemberId()
        if memberid:
            mtool = getToolByName(self, 'portal_membership')
            return mtool.getHomeUrl(id=memberid, verifyPermission=1)


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'ConditionalStringWidget_edit')
    def ConditionalStringWidget_edit(self, fieldname):
        "Edit mode for conditional string widget"
        if self.contactIsMember():
            return getattr(self, fieldname)
        return '<input type="text" name="%s" value="%s" size="30" maxlength="255">' % (fieldname, getattr(self, fieldname))


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'memberIdWidget')
    def memberIdWidget(self, fieldname):
        "Edit mode for conditional string widget"
        result = ['<select name="%s">' % fieldname]
        a = result.append
        a('    <option value=""> --- </option>')
        ids = self.getMemberItems()
        ids.sort()
        for id, fullname in ids:
            selected = ''
            if id == self.getMemberId():
                selected = ' selected=selected'
            a('    <option value="%s"%s>%s (%s)</option>' % (id, selected, fullname, id))
        a('</select>')
        return '\n'.join(result)


    # ignore, used for debugging
    security.declarePublic('test')
    def test(self):
        "Debugging helper"
        return repr(self._member_id)


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getContactOrganizations')
    def getContactOrganizations(self):
        "Returns list of members or organization objects based on query"
        result = []
        brains = self.portal_catalog(portal_type='mxmContactsOrganization')
        for brain in brains:
            try:
                obj = brain.getObject()
                if not obj is None:
                    result.append(obj)
            except:
                pass
        return result


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getOrganizationsDisplaylist')
    def getOrganizationsDisplaylist(self):
        "Returns list of members or organization objects based on query"
        orgs = self.getContactOrganizations()
        return [(o.UID(), o.title_or_id()) for o in orgs]



class mxmContactsOrganization(BaseContent):

    content_icon = 'mxmContacts_organization_icon.gif'

    global_allow = 0

    schema = mxmContacts_organization_schema

    actions = (
        {
            'id': 'view',
            'name': 'View',
            'action': 'string:${object_url}/mxmContacts_organization_view',
            'permissions': (VIEW_CONTENTS_PERMISSION,)
        },
        {
            'id': 'metadata',
            'name': 'Properties',
            'action': 'string:${object_url}/base_metadata',
            'permissions': (EDIT_CONTENTS_PERMISSION,),
            'visible':0,
        },
        {
            'id': 'vcard_export',
            'name': 'vcard export',
            'action': 'string:${object_url}/vcard_export',
            'permissions': (VIEW_CONTENTS_PERMISSION,),
            'visible':1,
        },
    )

    # Standard security settings
    security = ClassSecurityInfo()
    security.declareObjectProtected(VIEW_CONTENTS_PERMISSION)



    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'summary')
    def summary(self):
        "Returns short title/description"
        return self.Title()

    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'Description')
    Description = summary

    security.declarePublic('getSlaveVocab')
    def getRegionVocab(self, master):
        return REGIONS[master]        

    def getEconomicblocVocab(self, master):
        return ECONOMICBLOCS[master]    
    

    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getContactPersons')
    def getContactPersons(self):
        "Returns list of ContactPerson under this organization"
        return self.getBRefs(relationship='mxmContacts_employed_at')
        
    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getContactResources')
    def getContactResources(self):
        "Returns list of Resources under this organization"
        return self.getBRefs(relationship='resource_owner')        

    def _site_encoding(self):
        "Returns the site encoding"
        portal_properties = self.portal_properties
        site_props = portal_properties.site_properties
        return site_props.default_charset or 'utf-8'

    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'vcard_export')
    def vcard_export(self):
        "Exports contact data in VCARD format"
        result = vCardExporter().vcard_Companies([self])
        setheader = self.REQUEST.RESPONSE.setHeader
        setheader('Content-Length', len(result))
        setheader('Content-Type', 'text/directory; charset=\"%s\"' % self._site_encoding())
        setheader('Content-Disposition', 'filename=%s.vcf' % self.getId())
        return result




class mxmContactsResource(BaseContent):

    content_icon = 'mxmContacts_resource_icon.gif'

    global_allow = 0

    schema = mxmContacts_resource_schema

    actions = (
        {
            'id': 'view',
            'name': 'View',
            'action': 'string:${object_url}/mxmContacts_resource_view',
            'permissions': (VIEW_CONTENTS_PERMISSION,)
        },
        {
            'id': 'metadata',
            'name': 'Properties',
            'action': 'string:${object_url}/base_metadata',
            'permissions': (EDIT_CONTENTS_PERMISSION,),
            'visible':0,
        },
    )

    # Standard security settings
    security = ClassSecurityInfo()
    security.declareObjectProtected(VIEW_CONTENTS_PERMISSION)

    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'summary')
    def summary(self):
        "Returns short title/description"
        return self.Title()

    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'Description')
    Description = summary

    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getPersons')
    def getPersons(self):
        "Returns list of persons who work in this resource/department"
        result = []
        brains = self.portal_catalog(portal_type='mxmContactsPerson')
        for brain in brains:
            try:
                obj = brain.getObject()
                if not obj is None:
                    if obj.getDepartment().id == self.id:
                        result.append(obj)
            except:
                pass
        return result


class mxmContactsGroup(BaseContent):

    """
    Collects a list of contact persons. It can pull out members as csv or mass-
    mail them.
    """

    content_icon = 'mxmContacts_group_icon.gif'

    global_allow = 0

    schema = mxmContacts_group_schema

    actions = (
        {
            'id': 'view',
            'name': 'View',
            'action': 'string:${object_url}/mxmContacts_group_view',
            'permissions': (VIEW_CONTENTS_PERMISSION,)
        },
        {
            'id': 'mxmContacts_group_form',
            'name': 'Add members',
            'action': 'string:${object_url}/mxmContacts_group_form',
            'permissions': (EDIT_CONTENTS_PERMISSION,)
        },
        {
            'id': 'metadata',
            'name': 'Properties',
            'action': 'string:${object_url}/base_metadata',
            'permissions': (EDIT_CONTENTS_PERMISSION,),
            'visible':0,
        },
        {
            'id' : 'mailform',
            'name' : 'Mailform',
            'action' : 'string:${object_url}/mxmContacts_mailForm',
            'permissions' : (VIEW_CONTENTS_PERMISSION,),
            'visible' : 1,
        },
        {
            'id' : 'export',
            'name' : 'Export',
            'action' : 'string:${object_url}/mxmContacts_group_csv',
            'permissions' : (VIEW_CONTENTS_PERMISSION,),
            'visible' : 1,
        },
        {
            'id': 'vcard_export',
            'name': 'vcard export',
            'action': 'string:${object_url}/vcard_export',
            'permissions': (VIEW_CONTENTS_PERMISSION,),
            'visible':1,
        },
    )

    # Standard security settings
    security = ClassSecurityInfo()
    security.declareObjectProtected(VIEW_CONTENTS_PERMISSION)


    security.declarePublic('getContactGroup')
    def getContactGroup(self):
        "Returns self"
        return self


    # relationship for ContactPerson
    MEMBER_REF = 'mxmContacts_group_member'

    security.declareProtected(EDIT_CONTENTS_PERMISSION, 'getContacts')
    def getContacts(self):
        """
        Returns the contact in this contact group
        """
        refs = self.getRefs(relationship=self.MEMBER_REF)
        return refs


    security.declareProtected(EDIT_CONTENTS_PERMISSION, 'setContactsByUid')
    def setContactsByUid(self, ids=None, REQUEST=None):
        """
        Set a list of Contactperson by uid
        """
        if ids:
            for uid in ids:
                self.addReference(uid, self.MEMBER_REF)
        if not REQUEST is None:
            REQUEST.RESPONSE.redirect('%s/mxmContacts_group_form' % self.absolute_url())
        

    security.declareProtected(EDIT_CONTENTS_PERMISSION, 'delContactsByUid')
    def delContactsByUid(self, uids=None, REQUEST=None):
        """
        Set a list of Contactperson by uid
        """
        if uids:
            for uid in uids:
                self.deleteReference(uid, self.MEMBER_REF)
        if not REQUEST is None:
            REQUEST.RESPONSE.redirect('%s/mxmContacts_group_form' % self.absolute_url())


    security.declareProtected(EDIT_CONTENTS_PERMISSION, 'contactSearch')
    def contactSearch(self):
        "Returns list of members or organization objects based on query"
        req = self.REQUEST
        SearchableText = req.get('SearchableText', '')
        portal_type = req.get('portal_type', '')
        result = []
        brains = self.portal_catalog(portal_type=portal_type, SearchableText=SearchableText)
        for brain in brains:
            try:
                obj = brain.getObject()
                if not obj is None:
                    result.append(obj)
            except:
                pass
        return result


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'getContactPersonSchema')
    def getContactPersonSchema(self):
        "Returns the contact persons schema"
        return mxmContacts_person_schema


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'contactPersonFields')
    def contactPersonFields(self):
        """
        Returns list of contactPerson fiels
        """
        return mxmContacts_person_schema.filterFields(isMetadata=0)


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'contactPersonFieldNames')
    def contactPersonFieldNames(self):
        """
        Returns list of contactPerson field names
        """
        return [field.getName() for field in self.contactPersonFields()]
        


    security.declarePrivate('anonymousInvokeFactory')
    def anonymousInvokeFactory(self, container, type_name, id, REQUEST=None, *args, **kw):
        """
        Anonymous cannot add objects with InvokeFactory, so this is a special 
        method to do it with. Must be called from other function to limit 
        possibillities of abuse.
        """
        user = self.getWrappedOwner()
        newSecurityManager(REQUEST, user)
        container.invokeFactory(type_name, id, REQUEST=REQUEST, *args, **kw)
        


    security.declarePublic('addAnonymousContact')
    def addAnonymousContact(self, fieldnames=None, REQUEST=None):
        """
        Lets anonymous users add contacts
        """
        if self.getAnonymousForm():
            message = '?portal_status_message=%s'
            if fieldnames is None:
                fieldnames = []
            kw = {}
            for fieldname in fieldnames:
                kw[fieldname] = REQUEST[fieldname]
            # generate an id
            now = self.ZopeTime().strftime('%Y%m%dZ%H%M%S')
            id = 'contactperson.%s.%s' % (now, random.randint(1,1000000))
            self.anonymousInvokeFactory(self.aq_parent, 'mxmContactsPerson', id, REQUEST=REQUEST, **kw)
            newContact = getattr(self, id)
            # set attributes
            # security check. Otherwise Anonymous could set any attribute
            legalFieldNames = set(self.contactPersonFieldNames())
            for key, val in kw.items():
                if key in legalFieldNames: 
                    setattr(newContact, key, val)
            # Relate to current group
            self.setContactsByUid([newContact.UID()])
            # reindex in the catalog
            newContact.reindexObject()
            # Redirect back to form
            absolute_url = self.absolute_url()
            message = message % 'Sucessfully added contact'
            REQUEST.RESPONSE.redirect('%s/mxmContacts_anonymous%s' % (absolute_url, message) )
        

    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'exportCSV')
    def exportCSV(self, contacts=None, fields=None, delimiter='comma', 
                  quote_char='double_quote', coding='latin-1',
                  export_type='mxmContactsPerson'):

        """
        Exports a list of contacts as a CSV file.
        contacts: if None it exports all contact persons in the folder.
        fields: field names to export
        """
        if contacts is None:
            contacts = self.listFolderContents(contentFilter={'portal_type':export_type})
        delim_map = {'tabulator':'\t', 'semicolon':';', 'colon':':', 'comma':',', 'space':' ', }
        delimiter = delim_map[delimiter]
        quote_map = {'double_quote':'"', 'single_quote':"'", }
        quote_char = quote_map[quote_char]
        # generate result
        if fields is None:
            result = ''
        else:
            rows = [fields]
            for contact in contacts:
                row = []
                for fieldname in fields:
                    field = contact.schema[fieldname]
                    value = getattr(contact, field.accessor)()
                    if field.type == 'reference':
                        if value:
                            if isinstance(value, ListType):
                                value = ', '.join([v.title_or_id() for v in value])
                            else:
                                value = value.title_or_id()
                        else:
                            value = ''
                    if value.find('\r\n') != -1:
                        value.replace('\r\n', '\n')
                    row.append(value)
                rows.append(row)
            # convert lists to csv string
            ramdisk = StringIO()
            writer = csv.writer(ramdisk, delimiter=delimiter)
            writer.writerows(rows)
            result = ramdisk.getvalue()
            ramdisk.close()
        # encode the result
        charset = self._site_encoding()
        if coding:
            result = result.decode(charset).encode(coding)
        else:
            coding = charset
        # set headers and return
        setheader = self.REQUEST.RESPONSE.setHeader
        setheader('Content-Length', len(result))
        setheader('Content-Type', 'text/x-comma-separated-values; charset=%s' % coding)
        setheader('Content-Disposition', 'filename=%s.csv' % self.getId())
        return result


    def _site_encoding(self):
        "Returns the site encoding"
        portal_properties = self.portal_properties
        site_props = portal_properties.site_properties
        return site_props.default_charset or 'utf-8'


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'vcard_export')
    def vcard_export(self):
        "Exports contact data in VCARD format"
        result = vCardExporter().vCard_Personals(self.getContacts())
        setheader = self.REQUEST.RESPONSE.setHeader
        setheader('Content-Length', len(result))
        setheader('Content-Type', 'text/directory; charset=\"%s\"' % self._site_encoding())
        setheader('Content-Disposition', 'filename=%s.vcf' % self.getId())
        return result


    security.declareProtected(VIEW_CONTENTS_PERMISSION, 'sendMail')
    def sendMail(self, subject='', body='', mfrom='', REQUEST=None):
        "Sends an email to all members of the contact group"
        portal = self.portal_url.getPortalObject()
        site_encoding = portal.portal_properties.site_properties.default_charset or 'utf-8'
        ##################
        # Create the message
        msg = Message()
        msg.set_payload(body, site_encoding)
        ##################
        # Get 'to' adresses
        to = [c.getEmail() for c in self.getContacts() if c.getEmail()]
        if not mfrom:
            mfrom = portal.email_from_address
        ########################
        # set headers on message
        msg['From'] = mfrom
        msg['Reply-To'] = mfrom
        msg['To'] = ','.join(to)
        msg['Date'] = self.ZopeTime().rfc822() # needed by some servers
        msg['Subject'] = Header(subject, site_encoding)
        ##################
        # Send the message
        mh = self.MailHost
        SMTPserver = mh.smtp_host
        SMTPport = mh.smtp_port
        outbox = smtplib.SMTP(SMTPserver, SMTPport)
        try:
            outbox.sendmail(mfrom, to, msg.as_string())
            success = 1
        except:
            success = 0
        #################################
        # returns a portal status message
        if REQUEST:
            if success:
                message = 'Succes! The message was sent'
            else:
                message = 'Error! The message could not be sent'
            REQUEST.RESPONSE.redirect(self.absolute_url() + '/mxmContacts_mailForm?portal_status_message=%s' % message)


    security.declarePublic('anonymousDeleteContactByUid')
    def anonymousDeleteContactByUid(self, contactUID, REQUEST=None):
        """
        This makes it possible for anonymous users to delete a contact from the 
        contact group if they know the contacts UID. This is practical if a 
        contactgroup is used as a newsletter, so that subscribers can 
        unsubscribe by clicking on a link. The contactUID is practically 
        impossible to guess. So it should be pretty safe.
        """
        if self.getanonymousUnsubscribe():
            self.delContactsByUid([contactUID])



setDefaultRoles(ADD_CONTENTS_PERMISSION,  ('Manager', 'Owner',))
setDefaultRoles(EDIT_CONTENTS_PERMISSION, ('Manager', 'Owner',))
setDefaultRoles(VIEW_CONTENTS_PERMISSION, ('Manager', 'Owner', 'Anonymous'))


registerType(mxmContacts, PROJECTNAME)
registerType(mxmContactsPerson, PROJECTNAME)
registerType(mxmContactsOrganization, PROJECTNAME)
registerType(mxmContactsResource, PROJECTNAME)
registerType(mxmContactsGroup, PROJECTNAME)



